跨平台GUI开发 - Qt
开发环境搭建
下载Qt Online Installer,如果Installer(安装或更新)下载速度太慢,可以选择 镜像源:
.\MaintenanceTool.exe --mirror https://mirrors.tuna.tsinghua.edu.cn/qt/
# 比较快
.\qt-online-installer-windows-x64-4.9.0.exe --mirror http://mirrors.ustc.edu.cn/qtproject/
注意:Windows下安装时,需要以管理员权限启动,否则安装Windows Debugger以及Runtime组件时,会一直提示无权限安装失败。
Ubuntu下首先需要安装一下软件包:
sudo apt-get install libgl1-mesa-dev
也可以通过命令行安装Qt。
QML模块化开发
假设我有这么一个模块:
Fluent
├── CMakeLists.txt
├── Rectangle.cpp
├── Rectangle.h
└── qml
└── Text.qml
那么 CMakeLists.txt 应该这么写:
cmake_minimum_required(VERSION 3.29) # 目前发现 CMake 低版本不能正常使用 qt_add_qml_module()
add_library(Fluent
# 其他源文件
)
qt_add_qml_module(Fluent
URI Fluent
VERSION 1.0
PLUGIN_TARGET FluentPlugin # 如果不指定,那么默认就是 TARGET+"plugin",即 Fluentplugin
SOURCES
Rectangle.h Rectangle.cpp
QML_FILES
qml/Text.qml
)
这样会生成两个静态库文件,以Windows下为例,一个是 Fluent.lib,一个是 Fluentplugin.lib。所以我们在链接应用程序时,指定这个两个静态库,即可成功使用。
如果是生成动态库文件,则可以将 Fluentplugin.dll 拷贝值可执行应用程序,即可加载。(正确性待确定)
QML 常用属性
DefaultProperty
Q_CLASSINFO("DefaultProperty", "contentData"):设置默认属性。所有的QML组件属性都是 key-value 形式的。之所以我们能看到如下形式:
Item {
id: control
Rectangle {
id: rect
}
}
是因为 Item 的实现中声明了 data : list<QtObject> 作为其默认属性,内部所有没有指明 key 的对象,都将加入data数组。
QML_ATTACHED
QML_ATTACHED(attached):QML中一个比较有意思的属性,在刚开始使用的时候 也许会有点困惑,例如:
Window {
Item: {
height: Window.height/2
Keys.onReturnPressed: console.log("Return key was pressed")
}
Keys { } // 错误,无法创建
}
为什么 Window 可以被实例化,Item 又能使用附加属性 Window.height。但是 Keys 不能实例化,而 Item 可以使用附加属性 Keys.onReturnPressed 呢?就初次查看其C++源码实现,可能也会有点疑惑。
在 quick/items/qquickwindowmodule_p.h 我们看到了 QML_ATTACHED(QQuickWindowAttached),其实上述示例代码中 Item 内部创建了一个 key 为 Window 的 QQuickWindowAttached 实例。所以两者其实根本就不是同一个类型,这都是为了 QML 的语法而设计的。
在 quick/items/qquickitem_p.h 能看到 QML_ATTACHED(QQuickKeysAttached),所以 Item 内部其实创建了一个 key 为 Keys 的 QQuickKeysAttached实例。那为什么 Keys 无法被创建呢,其实QML组件 Keys 对应的 C++ 类也是 QQuickKeysAttached,而其通过 QML_UNCREATABLE 标记为不可创建 。
注意的是,附加属性都是 QObject 的子类,QQuickItem 成为附加属性是不对的。这也是为什么 Item、Window 这样的组件需要使用其名作为附加属性时,需要新建一个 QObject 子类,然后在其内部使用 QML_ATTACHED 声明附加属性名。而 Keys 可以直接在其内部通过 QML_ATTACHED 声明自己作为附加属性名。
再举一个例子:
RowLayout {
Item {
Layout.maximumWidth: 200
Layout.fillWidth: true
}
Item {
Layout.minimumWidth: 100
Layout.fillHeight: true
}
}
其实每个 Item 内都创建了一个 key 为 Layout 的 QQuickLayoutAttached 对象(在 QQuickLayout 通过 QML_ATTACHED 声明),RowLayout 在计算布局时,就会通过 qmlAttachedPropertiesObject<QQuickLayout>() 函数获取每个子控件的 QQuickLayoutAttached 对象,然后根据属性布局。
更多用法可以参考Qt帮助手册,这里稍微记录一下 QQuickAttachedPropertyPropagator 类,通过它的 propagator(传播)特性来创建控件主题是非常比较合适的。
无边框
在 Qt 开发,对于一些较真的 UI 设计师,经常会设计一下需要自定义标题栏的需求。Qt 并没有提供自定义窗口标题栏的实现,更不用说跨平台了。这是因为 Windows 使用 DWM(Desktop Window Manager)管理窗口程序的移动,缩放,标题栏,边框效果。Linux 上窗口管理系统因为发行系统多样化,其则更加多样化了。
在网上也有很多人尝试自己封装跨平台的 Qt 无边框窗口类,例如 QWindowKit,这是中国程序员发起的,我看也有很多人用,不过大部分人都在提 issue,我也拉下来试用了一下,发现其确实在不同Windows版本之前,不用Qt版本之间,多台显示器(还可能分辨率不一致,DPI不一致)会遇到各种各样的问题,表现不一致甚至带来不能使用的BUG。但不管怎样,它在尝试统一封装跨平台。
最后,就只能自己针对每种平台的桌面窗口管理系统(DWM),进行特定平台的代码开发了。这里可以提一下 FluentUI,他使用 Qt QML 实现了 Fluent 设计风格的 Qt 组件。最开始它也是使用的 QWindowKit 作为无边框实现,但是看 commit 应该是 QWindowKit 作者对该项目做了一些迁移改动,FluentUI 就自己实现了 Windows 下的无边框窗口实现,并自定义标题栏。顺便提一下这个库的控件样式很棒,作者也是中国人。
善用 QVariantList 和 QVariantMap
当我们希望将 C++ 中的数据结构传递到 QML 中时,一般通常做的方式是,使用 Q_OBJECT、Q_GADGET实现一个数据结构,好让 QML 能够认识它,但是这样呢,比较麻烦。在一个稍微大一点的项目中,肯定有很多数据结构需要交换。
这个时候使用 QVariantList 和 QVariantMap 可以省去重新实现一个类的繁琐工作。
class Manager : public QObject {
Q_OBJECT
public:
QVariantList availableUsbVideoCameras() const {
QVariantList ret;
auto devices = cameras();
for (auto &device : devices) {
QVariantMap item;
item.insert("name", device.name);
item.insert("path", device.path);
ret << item;
}
return ret;
}
};
ComboBox {
textRole: "name"
model: Manager.availableUsbVideoCameras()
}
多语言支持 Qt Linguist
Qt for Android
在搭建 Qt for Android 开发环境时,我们可以设置环境变量ANDROID_SDK_HOME为D:\Android。这样,一些文件,例如AVD,就不会默认存放在C盘,而放在D:\Android文件夹下,在这里,我也将Android SDK和NDK放在了这个文件夹,以便以后统一管理。
Qt Creator第一次在编译Android程序时,会提示需要下载Gradle:
Generating Android Package
Input file: E:/Projects/untitled1-Qt_6_5_0_Clang_arm64_v8a-Debug/android-appuntitled1-deployment-settings.json
Output directory: E:/Projects/untitled1-Qt_6_5_0_Clang_arm64_v8a-Debug/android-build/
Application binary: appuntitled
Android build platform: android-31
Install to device: No
Downloading https://services.gradle.org/distributions/gradle-8.0-bin.zip
但是通常由于国内GFW的问题,会导致下载失败。我们可以手动下载上述打印的Gradle压缩包,然后不解压直接放在 C:\Users\你的账户\.gradle\wrapper\dists\gradle-8.0-bin\ca5e32bp14vu59qr306oxotwh 下,这个目录可能会随着版本变化而变化(但是 C:\Users\你的账户\.gradle\wrapper\dists\gradle-x.x-bin 是不会发生改变的),需要注意。
打包
windeployqt FactoryTool.exe
dumpbin /DEPENDENTS FactoryTool.exe
脱坑经验
插件调试
Qt为了跨平台,使用了plugin插件的方式对各个库进行集成,在实际部署的时候,我们编写的程序运行时可能缺少插件依赖的库或对应库的版本不对,使得程序崩溃且不输出相关错误信息,这时我们需要设置环境变量:
export QT_DEBUG_PLUGINS=1
就可以看到Qt程序加载各个插件的过程,看到详细的报错信息。
源码编译
参考 Qt for Linux/X11 编译。
安装必要的依赖包:
libgl1-mesa-dev
参考 Qt for X11 Requirements 安装 xcb qpa插件的依赖包:
apt install \
libfontconfig1-dev \
libfreetype-dev \
libgtk-3-dev \
libx11-dev \
libx11-xcb-dev \
libxcb-cursor-dev \
libxcb-glx0-dev \
libxcb-icccm4-dev \
libxcb-image0-dev \
libxcb-keysyms1-dev \
libxcb-randr0-dev \
libxcb-render-util0-dev \
libxcb-shape0-dev \
libxcb-shm0-dev \
libxcb-sync-dev \
libxcb-util-dev \
libxcb-xfixes0-dev \
libxcb-xkb-dev \
libxcb1-dev \
libxext-dev \
libxfixes-dev \
libxi-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev \
libxrender-dev
参考 Qt for Wayland Requirements 安装 wayland qpa插件的依赖包:
sudo apt install libwayland-dev libwayland-egl1-mesa libwayland-server0 libgles2-mesa-dev libxkbcommon-dev
得益于 CMake 的引入,Qt6 的编译比 Qt5 方便很多:
tar xvf qt-everywhere-src-6.10.0.tar.xz
cd qt-everywhere-src-6.10.0
mkdir build && cd build
# -shared
../configure -prefix /opt/Qt6.10.0 -release -static -nomake examples -nomake tests -skip qtwebengine,qtwebview,qtwebchannel,qtlanguageserver,qt5compat,qtremoteobjects,qtspeech,qtgrpc
cmake --build . --parallel
cmake --install .
上述的 -skip 跳过的 module 其实也是 qt-everywhere-src-6.10.0 文件夹内的各个组件的文件夹名。
通过 build/config.summary 可以查看各组件支持情况:
...省略部分内容...
QPA backends:
DirectFB ............................... no
EGLFS .................................. yes
EGLFS details:
EGLFS OpenWFD ........................ no
EGLFS i.Mx6 .......................... no
EGLFS i.Mx6 Wayland .................. no
EGLFS RCAR ........................... no
EGLFS EGLDevice ...................... no
EGLFS GBM ............................ no
EGLFS VSP2 ........................... no
EGLFS Mali ........................... no
EGLFS Raspberry Pi ................... no
EGLFS X11 ............................ yes
LinuxFB ................................ yes
VNC .................................... yes
VK_KHR_display ......................... no
QNX:
lgmon ................................ no
IMF .................................. no
XCB:
Using system-provided xcb-xinput ..... no
GL integrations:
GLX Plugin ......................... yes
XCB GLX .......................... yes
EGL-X11 Plugin ..................... yes
...省略部分内容...
交叉编译
参考 Configure an Embedded Linux Device。
rk3566 sdk 内核版本为 4.19,受 struct statx 的 stx_mnt_id 字段加入,5.8 以上才支持。
xkbcommon 依赖包也比较老
mkdir build && cd build
../configure -release -opengl es2 -nomake examples -nomake tests \
-skip qtwebengine,qtwebview,qtwebchannel,qtlanguageserver,qt5compat,qtremoteobjects,qtspeech,qtgrpc,qtmultimedia \
-qt-host-path /opt/Qt6.10.0_Shared \
-extprefix $HOME/qt6-rpi \
-prefix /usr/local/qt6 \
-- -DCMAKE_TOOLCHAIN_FILE=/home/amass/Projects/Rebirth/resources/toolchain.cmake
cmake --build . --parallel
cmake --install .
export LD_LIBRARY_PATH=/usr/local/qt6/lib:$LD_LIBRARY_PATH